<?php
require_lib('util/req_util',false);
/**
 *  IP地理区域查询，使用纯真IP地址库查找区域。
 *  <br>示例:<br>
 *  require_lib('net/IPRegion.class',false);
 *  $ip_region=new IPRegion(ROOT_PATH .DS.'res/ip/qqwry.dat');
 *  print_r($ip_region->get_region());
 */
class IPRegion
{
    private $fp;		//dat文件指针
    private $first;		//int 第一条IP记录的偏移地址
    private $last;		//int 最后一条IP记录的偏移地址
    private $count;		//int IP记录的总条数（不包含版本信息记录）

    /**
     * 根据$file dat文件路径初始化，必须提供qqwry.dat文件路径，下载http://www.skycn.com/soft/14344.html
     */
    public function __construct($file) { 
    	if(! file_exists($file)) throw new Exception('IPRegion:qqwry.dat file not exists,NOT FOUND '.$file);
        $this->fp = NULL;
        if (($this->fp = fopen($file, 'rb')) !== false) {
            $this->first = $this->get_long();
            $this->last = $this->get_long();
            $this->count = ($this->last - $this->first) / 7;
        }
    }
    public function __destruct() {
        if ($this->fp) fclose($this->fp);
        $this->fp = NULL;
    }
    /**
     * 根据所给 IP 地址或域名返回所在地区信息
     * @param string $ip  IP 地址
     * @return array 所在地区信息array('ip'=>,'begin_ip'=>,'end_ip'=>,'region'=>,'isp'=>,)
     */
    public function get_region($ip='') {
        if (!$this->fp) return null;
		if(empty($ip)) $ip = get_client_ip();
        $region['ip'] = gethostbyname($ip);
        $ip = $this->pack($region['ip']);   // 将输入的IP地址转化为可比较的IP地址,不合法的IP地址会被转化为255.255.255.255
        
        // binsearch
        $l = 0;                         
        $u = $this->count;            
        $ip_pos = $this->last;								// 如果没有找到就返回最后一条IP记录
        while ($l <= $u) {									// 查找失败
            $i = floor(($l + $u) / 2);
            fseek($this->fp, $this->first + $i * 7);
            $begin_ip = strrev(fread($this->fp, 4));		// 获取中间记录的开始IP地址,strrev将little-endian的压缩IP地址转化为big-endian的格式

            if ($ip < $begin_ip) $u = $i - 1;				// 用户的IP小于中间记录的开始IP地址时,将搜索的上边界修改为中间记录减一
            else {
                fseek($this->fp, $this->get_long_long());
                $end_ip = strrev(fread($this->fp, 4));		// 获取中间记录的结束IP地址
                if ($ip > $end_ip)  $l = $i + 1; 
                else {                  					// 用户的IP在中间记录的IP范围内时
                    $ip_pos = $this->first + $i * 7;
                    break;              
                }
            }
        }

        //获取查找到的IP地理位置信息
        fseek($this->fp, $ip_pos);
        $region['begin_ip'] = long2ip($this->get_long());		// 用户IP所在范围的开始地址
        $offset = $this->get_long_long();
        fseek($this->fp, $offset);
        $region['end_ip'] = long2ip($this->get_long());			// 用户IP所在范围的结束地址
        $byte = fread($this->fp, 1);							// 标志字节
        switch (ord($byte)) {
            case 1:												// 标志字节为1，表示位置和ISP都被同时重定向
                $countryOffset = $this->get_long_long();		// 重定向地址
                fseek($this->fp, $countryOffset);
                $byte = fread($this->fp, 1);					// 标志字节
                switch (ord($byte)) {
                    case 2:										// 标志字节为2，位置重定向
                        fseek($this->fp, $this->get_long_long());
                        $region['region'] = $this->get_chars();
                        fseek($this->fp, $countryOffset + 4);
                        $region['isp'] = $this->get_isp();
                        break;
                    default:									// 位置没有被重定向
                        $region['region'] = $this->get_chars($byte);
                        $region['isp'] = $this->get_isp();
                        break;
                }
                break;
            case 2:												// 标志字节为2，位置被重定向
                fseek($this->fp, $this->get_long_long());
                $region['region'] = $this->get_chars();
                fseek($this->fp, $offset + 8);
                $region['isp'] = $this->get_isp();
                break;
            default:											// 位置没有重定向
                $region['region'] = $this->get_chars($byte);
                $region['isp'] = $this->get_isp();
                break;
        }
        if($region['region'] == ' CZ88.NET')	$region['region'] ='';	//没有找到有效信息
        if($region['isp'] == ' CZ88.NET')	$region['isp'] = '';
        return $region;
    }    
    /**
     * 得到读取的长整型数
     */
    private function get_long() {
        $result = unpack('Vlong', fread($this->fp, 4)); //将读取的little-endian编码的4个字节转化为长整型数
        return $result['long'];
    }

    /**
     * 得到读取的3个字节的长整型数
     */
    private function get_long_long() {
        $result = unpack('Vlong', fread($this->fp, 3).chr(0)); //将读取的little-endian编码的3个字节转化为长整型数
        return $result['long'];
    }
    /**
     * 得到读取的字符串
     * @param string $data
     * @return string
     */
    private function get_chars($data = "") {
        $char = fread($this->fp, 1);
        while (ord($char) > 0) {        // 字符串按照C格式保存，以\0结束
            $data .= $char;             // 将读取的字符连接到给定字符串之后
            $char = fread($this->fp, 1);
        }
        return iconv('gbk','utf-8',$data);
    }
    /**
     *得到压缩后可进行比较的IP地址
     * @param string $ip 
     * @return string
     */
    private function pack($ip) {
        return pack('N', intval(ip2long($ip))); //将IP地址转化为长整型数，如果IP地址错误,intval将返回false转化为整数-1，之后压缩成big-endian编码的字符串
    }
    /**
     * 得到地区信息
     */
    private function get_isp() {
        $byte = fread($this->fp, 1);    // 标志字节
        switch (ord($byte)) {
            case 0:                     // 没有区域信息
                $area = "";
                break;
            case 1:
            case 2:                     // 标志字节为1或2，表示区域信息被重定向
                fseek($this->fp, $this->get_long_long());
                $area = $this->get_chars();
                break;
            default:                    // 否则，表示区域信息没有被重定向
                $area = $this->get_chars($byte);
                break;
        }
        return $area;
    }


}
